#!/usr/bin/env node
/**
 * TASKKILL /F /FI "IMAGENAME eq Node*"
 * 关闭所有nodejs进程
 */
'use strict';
/*buildContainer*/
process.chdir(__dirname);
const configs = require('./options.js')
const { execSync } = require('child_process');
const cluster = require('cluster');
if (cluster.isMaster) {
    try {
        execSync(`kill -9 $(netstat -tunlp | grep :${configs.http.port} | awk '{print$7}' | cut -d '/' -f 1)`);
    } catch (error) {

    }
}
const WebSocketServer = require('websocket').server;
const { Onvif, VideoStream } = require('./class/node-onvif.js');
const { MySQLUtils, RedisUtils } = require('./class/dbUtils.js');
const zlib = require('zlib');
const http = require('http');
const path = require("path");
const mkdirp = require("mkdirp");
const Express = require("express");
const bodyParser = require("body-parser");
const session = require('express-session');
const cookieParser = require('cookie-parser');
const fs = require('fs');

function isNull(objs, isTrim = false) {
    if (typeof(objs) === 'string') {
        if (isTrim === true) objs = objs.trim();
        return objs === '';
    }
    return objs === null || objs === undefined;
}
function toJSON(data) {
    try {
        if (data.startsWith('{') && data.endsWith('}')) {
            data = JSON.parse(data)
        }
    } catch (error) {
    }
    return data;
}
let devices = {};
const rtsps = {};
const onvif = new Onvif();
let Redis;

if (configs.globalCatchError === true) {
    process.on(`uncaughtException`, (err) => {
        configs.log(
            `uncaughtException`,
            `系统出现底层错误：\n--------------------------\n${err.stack}\n--------------------------`,
            `err`
        );
        if (err.message.indexOf(`address already in use`) > -1) {
            process.exit(1);
        }
    });
}
function startDiscovery(conn) {
    devices = {};
    let names = {};
    let types = {};
    let device_list = [];
    // device_list = [{
    //     xaddrs: ['http://192.168.20.65:80/onvif/device_service'],
    //     name: 'Camera',
    //     type: 'hikvision'
    // }]
    // sendList(device_list);
    if (device_list.length === 0) {
        onvif.startProbe().then(device_list => {
            sendList(device_list);
        }).catch((error) => {
            let res = { 'id': 'startDiscovery', 'error': error.message };
            if (conn) {
                conn.send(JSON.stringify(res));
            } else {
                configs.log('startDiscovery', JSON.stringify(res), 'error')
            }
        });
    }
}
function sendList(device_list) {
    device_list.forEach((device) => {
        let odevice = new onvif.OnvifDevice({
            xaddr: device.xaddrs[0]
        });
        let addr = odevice.address;
        devices[addr] = odevice;
        names[addr] = device.name;
        types[addr] = device.type;
    });
    let devs = {};
    for (let addr in devices) {
        devs[addr] = {
            name: `[${types[addr]}]${names[addr]}`,
            address: addr
        }
    }
    let res = { 'id': 'sendList', 'result': devs };
    if (conn) {
        conn.send(JSON.stringify(res));
    } else {
        configs.log('sendList', JSON.stringify(res), 'debug')
    }
}
async function connect(conn, params, that) {
    let device = devices[params.address];
    let videoSizeRate = configs.rtspWsStream?.videoSizeRate || 0.6;
    let options = {
        streamUrl: params.rtsp || '',
        ffmpegOptions: {}
    }
    if (params.rtsp && params.rtsp !== '') {
        let rtsp = params.rtsp.trim();
        let name = params.address;
        if (rtsps[name] == undefined) {
            makeVideoStream(name, options, configs.rtspWsStream.http.port);
            await new Promise((resolve2, reject2) => {
                const interval = setInterval(() => {
                    try {
                        if (rtsps[name] && rtsps[name]['rtspStream']) {
                            // if (rtsps && rtsps?.name?.port && rtsps?.name?.port !== '') {
                            clearInterval(interval)
                            configs.log('makeVideoStream.success', `${options.streamUrl} => ${rtsps[name]['rtspStream']['output']}`, 'info')
                            resolve2();
                        }
                    } catch (error) { }
                }, 500);
            })
        }
    }
    if (!device) {
        // let res =  {'id': 'connect', 'error': 'The specified device is not found: ' + params.address};
        // if (conn) conn.send(JSON.stringify(res));
        // return;
        device = new onvif.OnvifDevice({
            xaddr: `http://${params.address}/onvif/device_service`
        });
        devices[params.address] = device;
    }
    if (params.user) {
        device.setAuth(params.user, params.pass);
    }
    if (params.rtsp && params.rtsp !== '') {
        options.streamUrl = params.rtsp;
        device.originRtsp = params.rtsp;
    }
    device.init(async (error, result) => {
        let rtspUrl = `${configs.rtspWsStream.baseUri}:${configs.rtspWsStream.http.port}`;
        if (configs.http.localIP === '[local]') {
            rtspUrl = configs.rtspWsStream.baseUri;
        }
        rtspUrl = `${rtspUrl}/live/${md5(params.rtsp ? params.rtsp : result?.rtsp || '')}.flv`; // `${configs.rtspWsStream.baseUri}${rtsps[name] ? ':' + rtsps[name].port : ''}`
        let res = { 'id': 'connect', result: { rtsp: rtspUrl } };
        if (error) {
            res['error'] = error.toString();
            configs.log('device.init', `${JSON.stringify(params)}: ${res['error']}`, 'error')
        } else {
            let name = `${params.address}`;
            if (that !== undefined) {
                that.rtsp = name;
                that.id = new Date().valueOf();
            }
            if (conn !== undefined && typeof (conn) !== 'function') {
                if (rtsps[name] == undefined) {
                    let options = {
                        name,
                        streamUrl: result['rtsp'],
                        width: parseInt(result.resolution.width * videoSizeRate),
                        height: parseInt(result.resolution.height * videoSizeRate),
                        ffmpegOptions: {}
                    }
                    makeVideoStream(name, options, configs.rtspWsStream.http.port);
                    await new Promise((resolve2, reject2) => {
                        const interval = setInterval(() => {
                            try {
                                if (rtsps[name] && rtsps[name]['rtspStream']) {
                                    // if (rtsps && rtsps?.name?.port && rtsps?.name?.port !== '') {
                                    clearInterval(interval)
                                    configs.log('makeVideoStream.success', `${options.streamUrl} => ${rtsps[name]['rtspStream']['output']}`, 'info')
                                    resolve2();
                                }
                            } catch (error) { }
                        }, 500);
                    })
                }
            }
            if (conn !== undefined && typeof (conn) !== 'function') {
                if (typeof (conn) !== 'function') {
                    getPresets(conn, {
                        address: params.address
                    })
                }
                if (that) {
                    rtsps[name][that.id] = "push";
                }
                delete result['rtsp'];
                res['result'] = {
                    ...result,
                    rtsp: rtspUrl
                };
            }
        }
        if (conn) {
            if (typeof (conn) === 'function') {
                conn(res);
            } else {
                conn.send(JSON.stringify(res));
            }
        } else {
            configs.log('connect', JSON.stringify(res), 'debug')
        }
    });
}
function makeVideoStream(name, options, wsPort) {
    rtsps[name] = {}
    options['wsPort'] = wsPort;
    rtsps[name]['port'] = wsPort;
    rtsps[name]['rtspStream'] = new VideoStream(options);
    rtsps[name]['rtspStream'].on('exit', () => {
        delete rtsps[name]
    })
    rtsps[name]['rtspStream'].on('exitWithError', () => {
        delete rtsps[name]
    })
}
// For Debug --------------------------------------------
//var total_size = 0;
//var start_time = 0;
//var total_frame = 0;
// ------------------------------------------------------

function fetchSnapshot(conn, params, that) {
    // For Debug --------------------------------------------
    //if(start_time === 0) {
    //	start_time = Date.now();
    //}
    // ------------------------------------------------------
    if (that.busy == true) return;
    let device = devices[params.address];
    if (!device) {
        let res = { 'id': 'fetchSnapshot', 'error': 'The specified device is not found: ' + params.address };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('fetchSnapshot', JSON.stringify(res), 'error')
        }
        return;
    }
    that.busy = true;
    device.fetchSnapshot((error, result) => {
        let res = { 'id': 'fetchSnapshot' };
        if (error) {
            res['error'] = error.toString();
        } else {
            var ct = result['headers']['content-type'];
            var buffer = result['body'];
            var b64 = buffer.toString('base64');
            var uri = 'data:' + ct + ';base64,' + b64;
            res['result'] = uri;

            // For Debug --------------------------------------------
            /*
            total_size += parseInt(result['headers']['content-length'], 10);
            var duration = Date.now() - start_time;
            var bps = total_size * 1000 / duration;
            var kbps = parseInt(bps / 1000);
            total_frame ++;
            var fps = Math.round(total_frame * 1000 / duration);
            console._log(kbps + ' kbps / ' + fps + ' fps');
            */
            // ------------------------------------------------------
        }
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('fetchSnapshot', JSON.stringify(res), 'debug')
        }
        that.busy = false
    });
}

function ptzMove(conn, params) {
    let device = devices[params.address];
    if (!device) {
        let res = { 'id': 'ptzMove', 'error': 'The specified device is not found: ' + params.address };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzMove', JSON.stringify(res), 'error')
        }
        return;
    }
    device.ptzMove(params, (error) => {
        let res = { 'id': 'ptzMove' };
        if (error) {
            res['error'] = error.toString();
        } else {
            res['result'] = true;
        }
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzMove', JSON.stringify(res), 'debug')
        }
    });
}

function ptzStop(conn, params) {
    let device = devices[params.address];
    if (!device) {
        let res = { 'id': 'ptzStop', 'error': 'The specified device is not found: ' + params.address };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzStop', JSON.stringify(res), 'error')
        }
        return;
    }
    device.ptzStop((error) => {
        let res = { 'id': 'ptzStop' };
        if (error) {
            res['error'] = error.toString();
        } else {
            res['result'] = true;
            Redis.set(`ptzStop_${params.address}`, new Date().valueOf());
        }
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzStop', JSON.stringify(res), 'debug')
        }
    });
}

function md5(data) {
    let hash = require('crypto').createHash('md5');
    hash.update(data, 'utf8');
    return hash.digest('hex');
}

function getOutput(conn, params) {
    let device = devices[params.address];
    if (!device) {
        let res = { 'id': 'getOutput', 'error': 'The specified device is not found: ' + params.address };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('getOutput', JSON.stringify(res), 'error')
        }
        return;
    }
    const profile = device.getCurrentProfile();
    const auth = `${device.user}:${device.pass}`
    let { rtsp } = profile?.stream;
    let md5Path = md5(rtsp)
    let { h, w, savePath, uri, videoDuration } = configs.snapshot;
    if (savePath.startsWith('.')) {
        savePath = path.join(__dirname, savePath)
    }
    savePath = path.join(savePath, md5Path)
    if (fs.existsSync(savePath) === false) {
        try {
            fs.mkdirSync(savePath, { recursive: true });
            configs.log('downPhoto', `目录${savePath}创建成功`, 'info')
        } catch (err) {
            configs.log('downPhoto', `目录${savePath}创建失败:${err.message}`, 'error')
        }
    }
    const streamInfo = path.join(savePath, 'stream.txt')
    if (fs.existsSync(streamInfo) === false) {
        fs.writeFileSync(streamInfo, rtsp, { encoding: 'utf8' });
    }
    let rtspAuth = rtsp.replace('rtsp://', `rtsp://${auth}@`)
    if (params.sec) {
        params.sec = Number(params.sec);
        if (isNaN(params.sec) === false) {
            videoDuration = params.sec;
        }
    }
    return { h, w, savePath, uri, videoDuration, rtsp, rtspAuth, md5Path }
}
function downVideo(conn, params) {
    let { h, w, savePath, uri, videoDuration, rtspAuth, md5Path } = getOutput(conn, params);
    let output = `${new Date().valueOf()}.mp4`
    ffmpeg(rtspAuth)
        .outputOptions([
            `-t ${videoDuration}`, // 设置输出视频的持续时间为15秒
            '-an',   // 禁用音频录制
            '-codec:v libx264',
            // '-preset ultrafast', // 编码速度非常快，但输出视频质量不高
            // '-preset superfast', // 编码速度非常快，但输出视频质量比ultrafast略微高一些
            // '-preset veryfast',  // 编码速度比较快，输出视频质量较好
            // '-preset faster',    // 编码速度比较快，输出视频质量比veryfast略微差一些
            // '-preset fast',      // 编码速度适中，输出视频质量较好
            '-preset medium',       // 编码速度适中，输出视频质量非常好，是默认值
            // '-preset slow',      // 编码速度较慢，但输出视频质量比medium更好
            // '-preset slower',    // 编码速度比slow略微慢一些，但输出视频质量更好
            // '-preset veryslow',  // 编码速度最慢，但输出视频质量最好
            '-crf 28',              // 设置输出视频的质量（可选，范围18-28，值越低质量越高）
            `-s ${w}x${h}`,  // 设置输出尺寸
        ])
        .output(path.join(savePath, output))
        .on('start', (commandLine) => {
            configs.log('downPhoto.ffmpeg', `Spawned Ffmpeg with command: ${commandLine}`);
        })
        .on('error', (err) => {
            let res = { 'id': 'downVideo', 'type': 'fail', data: err.message };
            if (conn) {
                conn.send(JSON.stringify(res));
            }
            configs.log('downPhoto.ffmpeg', 'An error occurred: ' + err.message, 'error');
        })
        .on('end', () => {
            let outUri = path.join(uri, md5Path, output)
            let res = { 'id': 'downVideo', data: outUri, 'type': 'success' };
            if (conn) {
                conn.send(JSON.stringify(res));
            }
            configs.log('downPhoto.ffmpeg', 'Video Save: ' + outUri, 'info');
        })
        .run();
}
function downPhoto(conn, params) {
    let { h, w, savePath, uri, rtsp, rtspAuth, md5Path } = getOutput(conn, params)
    let output = `${new Date().valueOf()}.jpg`
    ffmpeg(rtspAuth)
        .outputOptions([
            '-f image2',
            '-vframes 1', // 只捕获一帧
            '-crf 28', // 设置输出视频的质量（可选，范围18-28，值越低质量越高）
            `-s ${w}x${h}`  // 设置输出尺寸
        ])
        .output(path.join(savePath, output))
        .on('start', (commandLine) => {
            configs.log('downPhoto.ffmpeg', `Spawned Ffmpeg with command: ${commandLine}`);
        })
        .on('error', (err) => {
            let res = { 'id': 'downPhoto', 'type': 'fail', data: err.message };
            if (conn) {
                conn.send(JSON.stringify(res));
            }
            configs.log('downPhoto.ffmpeg', 'An error occurred: ' + err.message, 'error');
        })
        .on('end', () => {
            let outUri = path.join(uri, md5Path, output)
            let res = { 'id': 'downPhoto', data: outUri, 'type': 'success' };
            if (conn) {
                conn.send(JSON.stringify(res));
            }
            configs.log('downPhoto.ffmpeg', 'Photo Save: ' + outUri, 'info');
        })
        .run();
}

function ptzHome(conn, params) {
    let device = devices[params.address];
    if (!device) {
        let res = { 'id': 'ptzHome', 'error': 'The specified device is not found: ' + params.address };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzHome', JSON.stringify(res), 'error')
        }
        return;
    }
    if (isNull(device.services.ptz)) {
        let res = { 'id': 'ptzHome', 'error': 'The specified device does not support PTZ.' };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzHome', JSON.stringify(res), 'error')
        }
        return;
    }
    const ptz = device.services.ptz;
    const profile = device.getCurrentProfile();
    if (profile == null) {
        configs.log('ptzSetHome.device.getCurrentProfile', `${params.address} 摄像机Onvif信息获取失败`, 'error')
    } else {
        const pars = {
            'ProfileToken': profile['token'],
            'Speed': 1
        };
        ptz.gotoHomePosition(pars, (error, result) => {
            let res = { 'id': 'ptzHome' };
            if (error) {
                res['error'] = error.toString();
            } else {
                res['result'] = true;
            }
            if (conn) {
                conn.send(JSON.stringify(res));
            } else {
                configs.log('ptzHome', JSON.stringify(res), 'debug')
            }
        });
    }
}

function ptzSetHome(conn, params) {
    let device = devices[params.address];
    if (!device) {
        let res = { 'id': 'ptzSetHome', 'error': 'The specified device is not found: ' + params.address };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzSetHome', JSON.stringify(res), 'error')
        }
        return;
    }
    if (isNull(device.services.ptz)) {
        let res = { 'id': 'ptzSetHome', 'error': 'The specified device does not support PTZ.' };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzSetHome', JSON.stringify(res), 'error')
        }
        return;
    }
    const ptz = device.services.ptz;
    const profile = device.getCurrentProfile();
    if (profile == null) {
        configs.log('ptzSetHome.device.getCurrentProfile', `${params.address} 摄像机Onvif信息获取失败`, 'error')
    } else {
        const pars = {
            'ProfileToken': profile['token'],
        };
        ptz.setHomePosition(pars, (error, result) => {
            let res = { 'id': 'ptzSetHome' };
            if (error) {
                res['error'] = error.toString();
            } else {
                res['result'] = true;
            }
            if (conn) {
                conn.send(JSON.stringify(res));
            } else {
                configs.log('ptzSetHome', JSON.stringify(res), 'debug')
            }
        });
    }
}

function setPreset(conn, params) {
    let device = devices[params.address];
    if (!device) {
        let res = { 'id': 'setPreset', 'error': 'The specified device is not found: ' + params.address };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('setPreset', JSON.stringify(res), 'error')
        }
        return;
    }

    let token = params.token;
    let isadd = params.isadd;
    if (isNull(device.services.ptz)) {
        let res = { 'id': 'ptzHome', 'error': 'The specified device does not support PTZ.' };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzHome', JSON.stringify(res), 'error')
        }
        return;
    }
    const ptz = device.services.ptz;
    const profile = device.getCurrentProfile();
    if (profile == null) {
        configs.log('setPreset.device.getCurrentProfile', `${params.address} 摄像机Onvif信息获取失败`, 'error')
    } else {
        const pars = {
            'ProfileToken': profile['token'],
            'PresetToken': `${token}`,
            isadd,
        };
        if (isadd === true) {
            pars['Name'] = `预设位 ${token}`
        }
        let res = { 'id': 'setPreset', token };
        ptz.setPreset(pars, (error, result) => {
            if (error) {
                res['error'] = error.toString();
            } else {
                res['result'] = true;
            }
            if (conn) {
                conn.send(JSON.stringify(res));
            } else {
                configs.log('setPreset', JSON.stringify(res), 'error')
            }
        });
    }
}

function getPresets(conn, params) {
    let device = devices[params.address];
    if (!device) {
        let res = { 'id': 'getPresets', 'error': 'The specified device is not found: ' + params.address };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('getPresets', JSON.stringify(res), 'error')
        }
        return;
    }

    if (isNull(device.services.ptz)) {
        let res = { 'id': 'ptzHome', 'error': 'The specified device does not support PTZ.' };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzHome', JSON.stringify(res), 'error')
        }
        return;
    }
    const ptz = device.services.ptz;
    const profile = device.getCurrentProfile();
    if (profile == null) {
        configs.log('getPresets.device.getCurrentProfile', `${params.address} 摄像机Onvif信息获取失败`, 'error')
    } else {
        const pars = {
            'ProfileToken': profile['token'],
        }
        let res = { 'id': 'getPresets' };
        ptz.getPresets(pars, (error, result) => {
            // console.log(result) // ??????
            if (error) {
                res['error'] = error.toString();
            } else {
                let Preset = [];
                try {
                    Preset = result?.data?.GetPresetsResponse?.Preset;
                    Preset = JSON.stringify(Preset);
                    Preset = Preset.replace(/\{"\$":{/gi, '{');
                    Preset = Preset.replace('}}}}]', '}}]');
                    Preset = Preset.replace(/}}}},{/gi, '}},{');
                    Preset = Preset.replace(/},"Name"/gi, ',"Name"');
                    Preset = Preset.replace(/},"PTZPosition"/gi, ',"PTZPosition"');
                    Preset = JSON.parse(Preset);
                } catch (error) {
                    Preset = [];
                }
                res['list'] = Preset
                res['result'] = true;
            }
            if (conn) {
                conn.send(JSON.stringify(res));
            } else {
                configs.log('getPresets', JSON.stringify(res), 'error')
            }
        });
    }
}

function gotoPreset(conn, params) {
    let device = devices[params.address];
    if (!device) {
        let res = { 'id': 'gotoPreset.device', 'error': 'The specified device is not found: ' + params.address };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('gotoPreset', JSON.stringify(res), 'error')
        }
        return;
    }

    let token = params.token;
    if (isNull(device.services.ptz)) {
        let res = { 'id': 'ptzHome', 'error': 'The specified device does not support PTZ.' };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzHome', JSON.stringify(res), 'error')
        }
        return;
    }
    const ptz = device.services.ptz;
    const profile = device.getCurrentProfile();
    if (profile == null) {
        configs.log('gotoPreset.device.getCurrentProfile', `${params.address} 摄像机Onvif信息获取失败`, 'error')
    } else {
        const pars = {
            'ProfileToken': profile['token'],
            'PresetToken': `${token}`,
            'Speed': { 'x': 1.0, 'y': 1.0, 'z': 1.0 }
        };
        let res = { 'id': 'gotoPreset', token };
        ptz.gotoPreset(pars, (error, result) => {
            if (error) {
                res['error'] = error.toString();
            } else {
                res['result'] = true;
                if (conn) {
                    Redis.set(`ptzStop_${params.address}`, new Date().valueOf());
                }
            }
            if (conn) {
                conn.send(JSON.stringify(res));
            } else {
                configs.log('gotoPreset', JSON.stringify(res), 'debug')
            }
        });
    }
}

function removePreset(conn, params) {
    let device = devices[params.address];
    if (!device) {
        let res = { 'id': 'removePreset', 'error': 'The specified device is not found: ' + params.address };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('removePreset', JSON.stringify(res), 'error')
        }
        return;
    }

    let token = params.token;
    if (isNull(device.services.ptz)) {
        let res = { 'id': 'ptzHome', 'error': 'The specified device does not support PTZ.' };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzHome', JSON.stringify(res), 'error')
        }
        return;
    }
    const ptz = device.services.ptz;
    const profile = device.getCurrentProfile();
    if (profile == null) {
        configs.log('removePreset.device.getCurrentProfile', `${params.address} 摄像机Onvif信息获取失败`, 'error')
    } else {
        const pars = {
            'ProfileToken': profile['token'],
            'PresetToken': `${token}`,
        };
        let res = { 'id': 'removePreset', token };
        ptz.removePreset(pars, (error, result) => {
            if (error) {
                res['error'] = error.toString();
            } else {
                res['result'] = true;
            }
            if (conn) {
                conn.send(JSON.stringify(res));
            } else {
                configs.log('removePreset', JSON.stringify(res), 'debug')
            }
        });
    }
}

/* ------------------------------------------------------------------
* Method: absoluteMove(params[, callback]) 绝对移动
* - params:
*   - ProfileToken | String  | required |
*   - Position     | Object  | required | pan, tilt and zoom
*     - x          | Float   | required |
*     - y          | Float   | required |
*     - x          | Float   | required |
*   - Speed        | Object  | optional | pan, tilt and zoom
*     - x          | Float   | required |
*     - y          | Float   | required |
*     - x          | Float   | required |
*
* {
*   'ProfileToken': cam['ProfileToken'],
*   'Position'    : {'x': 0.5, 'y': -1, 'z': 0.5},
*   'Speed'       : {'x': 1, 'y': 1, 'z': 1}
* }
* ---------------------------------------------------------------- */
function absoluteMove(conn, params) {
    let device = devices[params.address];
    if (!device) {
        let res = { 'id': 'absoluteMove', 'error': 'The specified device is not found: ' + params.address };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('absoluteMove', JSON.stringify(res), 'error')
        }
        return;
    }
    if (isNull(device.services.ptz)) {
        let res = { 'id': 'ptzHome', 'error': 'The specified device does not support PTZ.' };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzHome', JSON.stringify(res), 'error')
        }
        return;
    }
    const ptz = device.services.ptz;
    const profile = device.getCurrentProfile();
    if (profile == null) {
        configs.log('absoluteMove.device.getCurrentProfile', `${params.address} 摄像机Onvif信息获取失败`, 'error')
    } else {
        let speed = 1;
        if (params['speed']) {
            speed = params['speed'];
        }
        const pars = {
            'ProfileToken': profile['token'],
            'Translation': params['Translation'],
            'speed': { x: speed, y: speed, z: speed },
        };
        let res = { 'id': 'absoluteMove', token };
        ptz.absoluteMove(pars, (error, result) => {
            if (error) {
                res['error'] = error.toString();
            } else {
                res['result'] = true;
            }
            if (conn) {
                conn.send(JSON.stringify(res));
            } else {
                configs.log('absoluteMove', JSON.stringify(res), 'info')
            }
        });
    }
}

/* ------------------------------------------------------------------
* Method: relativeMove(params[, callback]) 相对移动
* - params:
*   - ProfileToken | String  | required | 
*   - Translation  | Object  | required | pan, tilt and zoom
*     - x          | Float   | required |
*     - y          | Float   | required |
*     - x          | Float   | required |
*   - Speed        | Object  | optional | pan, tilt and zoom
*     - x          | Float   | required |
*     - y          | Float   | required |
*     - x          | Float   | required |
*
* {
*   'ProfileToken': 'Profile1',
*   'Translation' : {'x': 0.5, 'y': 1.0, 'z': 1.0},
*   'Speed'       : {'x': 1, 'y': 1, 'z': 1}
* }
* ---------------------------------------------------------------- */
function relativeMove(conn, params) {
    let device = devices[params.address];
    if (!device) {
        let res = { 'id': 'relativeMove', 'error': 'The specified device is not found: ' + params.address };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('relativeMove', JSON.stringify(res), 'error')
        }
        return;
    }
    if (isNull(device.services.ptz)) {
        let res = { 'id': 'ptzHome', 'error': 'The specified device does not support PTZ.' };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('ptzHome', JSON.stringify(res), 'error')
        }
        return;
    }
    const ptz = device.services.ptz;
    const profile = device.getCurrentProfile();
    if (profile == null) {
        configs.log('relativeMove.device.getCurrentProfile', `${params.address} 摄像机Onvif信息获取失败`, 'error')
    } else {
        let speed = 1;
        if (params['speed']) {
            speed = params['speed'];
        }
        const pars = {
            'ProfileToken': profile['token'],
            'Translation': params['Translation'],
            'speed': { x: speed, y: speed, z: speed },
        };
        let res = { 'id': 'relativeMove', token };
        ptz.relativeMove(pars, (error, result) => {
            if (error) {
                res['error'] = error.toString();
            } else {
                res['result'] = true;
            }
            if (conn) {
                conn.send(JSON.stringify(res));
            } else {
                configs.log('relativeMove', JSON.stringify(res), 'debug')
            }
        });
    }
}

function reboot(conn, params) {
    let device = devices[params.address];
    if (!device) {
        let res = { 'id': 'reboot', 'error': 'The specified device is not found: ' + params.address };
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('reboot', JSON.stringify(res), 'error')
        }
        return;
    }
    const dev = device.services.device;
    let res = { 'id': 'reboot' };
    dev.reboot((error, result) => {
        if (error) {
            res['error'] = error.toString();
        } else {
            res['result'] = true;
        }
        if (conn) {
            conn.send(JSON.stringify(res));
        } else {
            configs.log('reboot', JSON.stringify(res), 'debug')
        }
    });
}
if (cluster.isMaster) {
    console.clear();
    configs.log(`Master process`, `with PID: ${process.pid}`);
    Redis = new RedisUtils({
        ser: configs.redis.ser,
        port: configs.redis.port,
        passwd: configs.redis.pwd,
        ssh: configs.redis.ssh,
        log: configs.log,
    });
    function allowOrigin(req, res, next) {
        if (res && res.header) {
            if (configs.http.allowOrigin) {
                res.header("Access-Control-Allow-Origin", configs.http.allowOrigin)
            } else {
                if (req.environ?.HTTP_ORIGIN) {
                    res.header("Access-Control-Allow-Origin", req.environ?.HTTP_ORIGIN);
                }
            }
            res.header("Access-Control-Allow-Headers", "author,authorization,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,access_token")
            res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS")
            res.header("Access-Control-Allow-Credentials", true)
        }
    }

    function response(req, res, Code, useGzip, Msg, nolog) {
        if (useGzip !== false) useGzip = true;
        try {
            if (res.req) {
                res.statusCode = Code
                res.setHeader("Access-Control-Allow-Origin", configs.http.allowOrigin)
                res.setHeader("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With")
                res.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS")
                res.setHeader("Access-Control-Allow-Credentials", true)
                res.setHeader('Content-Type', 'text/html; charset=UTF-8')
                if (useGzip) res.setHeader('content-encoding', 'gzip')
                if (typeof Msg === `string`) {
                    Msg = {
                        Code,
                        Msg
                    }
                }
                const response = Msg
                Msg = JSON.stringify(Msg)
                if (Msg === "{}") Msg = ""
                if (configs.isDebug === true) {
                    let logStr = {
                        remoteAddress: req.client.remoteAddress,
                        remotePort: req.client.remotePort,
                        originalUrl: req.originalUrl,
                        userAgent: req.headers[`user-agent`] || `user-agent not set`,
                        body: req.body
                    }
                    logStr = JSON.parse(JSON.stringify(logStr).replace(/::ffff:/gi, ''));
                    res.once('finish', () => {
                        if (configs.isDebug === true) {
                            let length1 = req.headers['content-length'];
                            let length2 = res.getHeader('content-length');
                            if (length1 === undefined && isNaN(length1)) length1 = 0
                            if (length2 === undefined && isNaN(length2)) length2 = 0
                            const msg = {
                                request: {
                                    method: req.method,
                                    code: res.statusCode,
                                    reqContentLength: length1,
                                    resContentLength: length2,
                                    req: logStr
                                },
                                response
                            }
                            configs.log(
                                `httpServer`,
                                msg,
                                `debug`
                            );
                        }
                    })
                }
                if (useGzip) Msg = zlib.gzipSync(Msg)
                res.end(Msg)
            }
        } catch (error) {
            if (error.stack.indexOf("ERR_HTTP_HEADERS_SENT") === -1) {
                configs.log(`response`, error.stack, 'error');
            }
        }
    }
    (async () => {
        const workers = {}
        const pids = {}
        await new Promise((resolve, reject) => {
            Redis.on('error', err => {
                configs.log('Redis.connect', `连接Redis服务器失败：${err}`, 'error');
            }).on('connect', () => {
                configs.log('Redis.connect', "成功连接Redis服务器", 'info');
                const MySQL = new MySQLUtils({
                    host: configs.mysql.host,
                    port: configs.mysql.port || 3306,
                    user: configs.mysql.user,
                    password: configs.mysql.password,
                    database: configs.mysql.database,
                    mode: configs.mysql.mode,
                })
                let data = {
                    cameraIdList: []
                };
                MySQL.syncQuery(`SELECT
					id,
					ip,
					username,
				password 
				FROM
					bsc_camera bc 
				WHERE
					deleted = 1 
					AND id IN ( SELECT DISTINCT bsc_camera_id FROM bsc_camera_ysw ) 
					AND ip <> '' 
					AND username <> ''`).then(async res => {
                    if (res.code === 200) {
                        if (res?.result instanceof Array) {
                            data.cameraIdList = res.result.map(item => item.id);
                            let list = {}
                            const timeStamp = new Date().valueOf()
                            for (let item in res.result) {
                                item = res.result[item]
                                const res2 = await MySQL.syncQuery(`SELECT * from bsc_camera_ysw where bsc_camera_id = '${item.id}' and camera_json <>''`);
                                const bscCameraYswVoList = [];
                                if (res2?.result instanceof Array) {
                                    for (let item2 in res2.result) {
                                        item2 = res2.result[item2]
                                        let data = {
                                            id: item2.id,
                                            ip: item.ip,
                                            bscCameraId: item.id,
                                            username: item.username,
                                            password: item.password,
                                            lxfs: item2.lxfs,
                                            sj: item2.sj,
                                            sjjg: item2.sjjg,
                                            token: item2.camera_json
                                        }
                                        await Redis.remove(`actionIndex_${item.ip}`)
                                        await Redis.remove(`ptzStop_${item.ip}`)
                                        if (data['ip'] !== undefined && data['ip'] !== null && data['token']) {
                                            if (data['sj'] == null) {
                                                delete data['sj']
                                            } else {
                                                if (data['sj'].includes('T')) {
                                                    data['sj'] = data['sj'].split('T')[1]
                                                } else if (data['sj'].includes(' ')) {
                                                    data['sj'] = data['sj'].split(' ')[1]
                                                }
                                                if (data['sj'].includes('.')) {
                                                    data['sj'] = data['sj'].split('.')[0]
                                                }
                                            }
                                            if (data['sjjg'] == null) delete data['sjjg']
                                            bscCameraYswVoList.push(data)
                                            // console.log(bscCameraYswVoList)
                                        }
                                    }
                                }
                                if (bscCameraYswVoList.length > 0) {
                                    list[`bscCameraId${item.id}`] = {
                                        timeStamp,
                                        bscCameraYswVoList
                                    }
                                    // 创建一个新的 Worker，传递一个要执行的函数
                                    await new Promise((resolve, reject) => {
                                        workers[`bscCameraId${item.id}`] = {
                                            worker: cluster.fork(),
                                            id: `bscCameraId${item.id}`,
                                        }
                                        const workerOptions = {
                                            cmd: `register`,
                                            redis: configs.redis,
                                            id: `bscCameraId${item.id}`,
                                            ...list[`bscCameraId${item.id}`]
                                        }
                                        workers[`bscCameraId${item.id}`].worker.send(workerOptions);
                                        setTimeout(() => {
                                            resolve();
                                        }, configs.getRandomInt(200, 500))
                                    })
                                } else {
                                    delete list[`bscCameraId${item.id}`];
                                }
                            }
                            // Fork workers.
                            cluster.on('exit', (worker, code, signal) => {
                                if (worker.process.pid in pids) {
                                    configs.log(`cluster.exit`, `Worker ${worker.process.pid} died, Recreate child process in two seconds`, 'error');
                                    data = pids[worker.process.pid]
                                    setTimeout(() => {
                                        const { id } = data;
                                        workers[id] = {
                                            worker: cluster.fork(),
                                            id,
                                        }
                                        workers[id].worker.send(data);
                                    }, 2000);
                                } else {
                                }
                                configs.log(`cluster.exit`, `Worker ${worker.process.pid} died`);
                            });
                            cluster.on('message', (worker, msg) => {
                                data = toJSON(msg)
                                if (data instanceof Object) {
                                    const cmd = data['cmd'];
                                    switch (cmd) {
                                        case "register":
                                            const { pid } = data;
                                            delete data['pid']
                                            pids[pid] = data
                                            break;
                                    }
                                } else if (data instanceof String) {
                                    // console._log('Main thread received:', msg);
                                }
                            });
                            data = {
                                ...data,
                                ...list
                            }
                            Object.keys(data).forEach(async item => {
                                await Redis.set(item, data[item])
                                // console._log(await Redis.get(item))
                            });
                            resolve()
                            // console._log(JSON.stringify(data))
                        }
                    } else {
                        configs.log('mysql.connect', `连接MySQL服务器失败：${res.result}`, 'error');
                    }
                });
            })
        })
        /**
         * 启动数据中台服务
         */
        const app = Express();
        const sessionCookie = `nodeOnvifService`;
        const apiPath = '/api';

        const http_server = http.createServer(app);
        app.use(Express.json(configs.http.httpLimit));
        app.use(bodyParser.json(configs.http.httpLimit));
        app.use(bodyParser.urlencoded({
            extended: true, //扩展模式
            limit: configs.http.limit
        }));
        app.use(cookieParser(sessionCookie));
        app.use(session({
            secret: sessionCookie, //与cookieParser中的一致
            resave: true,
            saveUninitialized: true
        }));
        app.all((req, res, next) => {
            allowOrigin(req, res, next);
            if (req.method === "OPTIONS") {
                res.sendStatus(200)
                res.end()
            } else {
                next();
            }
        });
        let dir = path.join(__dirname, configs.http.webroot)
        if (__dirname.includes('/backend')) {
            dir = path.join(__dirname, `../dist${configs.http.webroot}`)
        }
        mkdirp.sync(dir)
        app.use(Express.static(dir)) // 读取静态资源
        const args = process.argv.splice(2);
        if (args && args.length > 0) {
            if (!isNaN(args[0])) {
                const port = parseInt(args[0]);
                if (port > 0 && port < 65535) {
                    configs.http.port = port;
                }
            }
        }

        http_server.listen(configs.http.port, function () {
            configs.log('http_server.listen', "Listening on port " + configs.http.port, 'info');
        });
        const wsserver = new WebSocketServer({
            httpServer: http_server,
        });
        app.post(`${apiPath}/rtsp`, async (req, res, next) => {
            let code = 200;
            if (req.body) {
                const { ip, username, password } = req.body;
                if (devices[ip] === undefined) {
                    await new Promise((resolve, reject) => {
                        connect(res => {
                            configs.log(`connect`, `连接并注册自动轮询的摄像机 ${ip}`, `info`)
                            resolve();
                        }, {
                            address: ip,
                            user: username,
                            pass: password
                        })
                    })
                }
                if (devices[ip]) {
                    const device = devices[ip]
                    let name = '';
                    await new Promise((resolve, reject) => {
                        device.init(async (error, result) => {
                            if (error) {
                                resolve()
                            } else {
                                name = `${result['Manufacturer']}:${result['SerialNumber']}`;
                                if (rtsps[name] == undefined) {
                                    let options = {
                                        name,
                                        streamUrl: result['rtsp'],
                                    }
                                    makeVideoStream(name, options, result, configs.rtspWsStream.http.port)
                                    await new Promise((resolve2, reject2) => {
                                        const interval = setInterval(() => {
                                            if (rtsps[name]['port'] && rtsps[name]['port'] !== '') {
                                                clearInterval(interval)
                                                resolve2();
                                            }
                                        }, 500);
                                    })
                                }
                                resolve();
                            }
                        });
                    })
                    response(req, res, code, configs.http.useGzip, {
                        code,
                        data: {
                            name,
                            uri: `${configs.rtspWsStream.baseUri}:${rtsps[name].port}`,
                            waitTime: configs.rtspWsStream.waitTime,
                        }
                    })
                }
            }
            next();
        })
        dir = path.join(__dirname, '/html')
        mkdirp.sync(dir)
        app.use(Express.static(dir)) // 读取静态资源
        const NodeMediaServer = require('node-media-server');
        const mediaConfig = {
            rtmp: configs.rtspWsStream.rtmp,
            http: configs.rtspWsStream.http,
            logType: 1,
        }
        this.nodeMediaServer = new NodeMediaServer(mediaConfig)
        this.nodeMediaServer.run()
        // this.nodeMediaServer.on('preConnect', (id, args) => {
        //   console.log('[NodeEvent on preConnect]', `id=${id} args=${JSON.stringify(args)}`);
        //   // let session = this.streamgetSession(id);
        //   // session.reject();
        // });

        // this.nodeMediaServer.on('postConnect', (id, args) => {
        //   console.log('[NodeEvent on postConnect]', `id=${id} args=${JSON.stringify(args)}`);
        // });

        this.nodeMediaServer.on('doneConnect', async (id, args, players) => {
            //console.log('[NodeEvent on doneConnect]', `id=${id} args=${JSON.stringify(args)}`);
            let count = 0;
            if (players instanceof Array) {
                players.forEach(item => {
                    if (item !== id) {
                        count++;
                    }
                })
            }
            if (count === 0) {
                if (args?.streamPath) {
                    const cmd = `pkill -f ${args.streamPath}`;
                    let killRety = 0;
                    while(killRety < 5) {
                        try {
                            execSync(cmd)
                            killRety = 5
                        } catch (error) {
                            await new Promise((resolve, rejects) => {
                                setTimeout(() => resolve(), 500);
                            });
                            killRety++;
                        }
                    }
                }
            }
        });

        // this.nodeMediaServer.on('prePublish', (id, StreamPath, args) => {
        //   console.log('[NodeEvent on prePublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
        //   // let session = this.streamgetSession(id);
        //   // session.reject();
        // });

        // this.nodeMediaServer.on('postPublish', (id, StreamPath, args) => {
        //   console.log('[NodeEvent on postPublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
        // });

        // this.nodeMediaServer.on('donePublish', (id, StreamPath, args) => {
        //   console.log('[NodeEvent on donePublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
        // });

        // this.nodeMediaServer.on('prePlay', (id, StreamPath, args) => {
        //   console.log('[NodeEvent on prePlay]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
        //   // let session = this.streamgetSession(id);
        //   // session.reject();
        // });

        // this.nodeMediaServer.on('postPlay', (id, StreamPath, args) => {
        //   console.log('[NodeEvent on postPlay]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
        // });

        // this.nodeMediaServer.on('donePlay', (id, StreamPath, args) => {
        //   console.log('[NodeEvent on donePlay]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
        // });
        wsserver.on('request', wsServerRequest);
        configs.log('System.start', '系统启动完成', 'info')
    })()
    function wsServerRequest(request) {
        const conn = request.accept(null, request.origin);
        if (conn) conn.on("message", function (message) {
            if (message.type !== 'utf8') {
                return;
            }
            const data = JSON.parse(message.utf8Data);
            const method = data['method'];
            const params = data['params'];
            if (method === 'startDiscovery') {
                startDiscovery(conn);
            } else if (method === 'connect') {
                connect(conn, params, this);
                // this.interval = setInterval(() => {
                // 	fetchSnapshot(conn, params, this);
                // }, 100)
            } else if (method === 'fetchSnapshot') {
                // fetchSnapshot(conn, params);
            } else if (method === 'ptzMove') {
                ptzMove(conn, params);
            } else if (method === 'ptzStop') {
                ptzStop(conn, params);
            } else if (method === 'ptzHome') {
                ptzHome(conn, params);
            } else if (method === 'downVideo') {
                downVideo(conn, params);
            } else if (method === 'downPhoto') {
                downPhoto(conn, params);
            } else if (method === 'ptzSetHome') {
                ptzSetHome(conn, params);
            } else if (method === 'setPreset') {
                setPreset(conn, params);
            } else if (method === 'gotoPreset') {
                gotoPreset(conn, params);
            } else if (method === 'removePreset') {
                removePreset(conn, params);
            } else if (method === 'reboot') {
                reboot(conn, params);
            } else if (method === 'absoluteMove') {
                absoluteMove(conn, params);
            } else if (method === 'relativeMove') {
                relativeMove(conn, params);
            }
        });

        if (conn) conn.on("close", function (message) {
            if (this.interval) {
                clearInterval(this.interval)
            }
        });
        if (conn) conn.on("error", function (error) {
            configs.log('wsServerRequest', error.message, 'error');
        });
    };
} else {
    process.on('message', (msg) => {
        const data = toJSON(msg)
        if (data instanceof Object) {
            const cmd = data['cmd'];
            switch (cmd) {
                case "register":
                    const worker = new turnWorker(data)
                    process.send({
                        ...data,
                        pid: process.pid
                    })
                    break;
            }
        } else if (data instanceof String) {
            configs.log('Main thread received:', msg);
        }
        // parentPort.postMessage('Hello from inline worker');
    });
    // let RedisUtils
    class turnWorker {
        constructor(config) {
            this.busy = false;
            this.id = config.id;
            this.config = config
            this.interval = undefined;
            this.camConfig = '';
            this.currentIndex = 0;
            this.Redis = new RedisUtils({
                ser: config.redis.ser,
                port: config.redis.port,
                passwd: config.redis.pwd,
                ssh: configs.redis.ssh,
                log: configs.log
            })
            this.Redis.on('error', err => {
                // console._log(err)
            }).on('connect', () => {
                configs.log('Redis.connect.id', `子进程${config.id}成功连接Redis服务器 with PID: ${process.pid}`, 'info');
                this.init();
            })
        }
        getDateTime(timeStamp) {
            if (typeof (timeStamp) === 'string') {
                timeStamp = new Date(timeStamp).valueOf()
            }
            const result = {
                timeStamp,
                datetime: configs.moment(timeStamp).format("YYYY-MM-DD HH:mm:ss"),
            }
            const temp = result['datetime'].split(" ")
            result['date'] = temp[0];
            result['time'] = temp[1];
            return result;
        }
        init() {
            if (this.interval !== undefined) {
                clearInterval(this.interval);
            }
            this.interval = setInterval(async () => {
                if (this.busy === false) {
                    this.busy = true;
                    const camConfig = await this.Redis.get(this.id);
                    this.busy = false;
                    const camConfigStr = JSON.stringify(camConfig);
                    if (this.camConfig !== camConfigStr) {
                        if (this.camConfig === '') {
                            configs.log('camConfig', 'load success', 'info')
                        } else {
                            configs.log('camConfig', 'update', 'info')
                        }
                        this.currentIndex = 0;
                        this.camConfig = camConfigStr;
                    }
                    if (camConfig.bscCameraYswVoList && camConfig.bscCameraYswVoList instanceof Array && camConfig.bscCameraYswVoList.length > 0) {
                        const infoData = camConfig.bscCameraYswVoList[0];
                        if (infoData) {
                            let today = configs.moment(new Date()).format("YYYY-MM-DD")
                            let now = new Date().valueOf();
                            // now = `${today} 02:02:03`;
                            now = this.getDateTime(now);
                            const { ip, username, password, lxfs } = infoData;
                            const actionObject = await this.Redis.get(`actionIndex_${ip}`, '');
                            if (lxfs === '时间节点') {
                                let isYesterday = false;
                                let currentIndex = -1;
                                // console.log(camConfig.bscCameraYswVoList, now)
                                camConfig.bscCameraYswVoList.forEach((item, index) => {
                                    const { sjjg, sj, token } = item;
                                    let datetime = configs.moment(`${today} ${sj}`).valueOf() //.format("YYYY-MM-DD HH:mm:ss")
                                    if (now.timeStamp - datetime > 0) {
                                        currentIndex = index;
                                    }
                                });
                                if (currentIndex === -1) {
                                    isYesterday = true;
                                    currentIndex = camConfig.bscCameraYswVoList.length - 1;
                                }
                                const { sjjg, sj, token } = camConfig.bscCameraYswVoList[currentIndex];
                                if (`${actionObject?.actionIndex}_${actionObject?.actionInfo?.token}` !== `${currentIndex}_${token}`) {
                                    await this.Redis.set(`actionIndex_${ip}`, {
                                        actionIndex: currentIndex,
                                        actionInfo: {
                                            ...camConfig.bscCameraYswVoList[currentIndex],
                                            timeStamp: now.timeStamp
                                        }
                                    });
                                    if (devices[ip] === undefined) {
                                        connect(res => {
                                            if (res.error) {

                                            } else {
                                                setTimeout(() => {
                                                    gotoPreset(undefined, {
                                                        address: ip,
                                                        token,
                                                        Speed: { 'x': 1.0, 'y': 1.0, 'z': 1.0 }
                                                    })
                                                    configs.log(`gotoPreset`, `移动摄像机[${ip}]位到：${isYesterday ? '昨日' : '今日'}[${configs.moment(`${today} ${sj}`).format("HH:mm:ss")}]的预置位 ${token}`, `info`);
                                                }, 500)
                                                configs.log(`connect`, `连接并注册自动轮询的摄像机 ${ip}`, `info`);
                                            }
                                        }, {
                                            address: ip,
                                            user: username,
                                            pass: password
                                        })
                                    } else {
                                        gotoPreset(undefined, {
                                            address: ip,
                                            token,
                                            Speed: { 'x': 1.0, 'y': 1.0, 'z': 1.0 }
                                        })
                                        configs.log(`gotoPreset`, `移动摄像机[${ip}]位到：${isYesterday ? '昨日' : ''}[${configs.moment(`${today} ${sj}`).format("HH:mm:ss")}]的预置位 ${token}`, `info`)
                                    }
                                }
                            } else {
                                let currentIndex = 0;
                                let changed = false;
                                if (actionObject instanceof Object) {
                                    currentIndex = actionObject['actionIndex'];
                                    const actionInfo = actionObject['actionInfo'];
                                    const { timeStamp, sjjg } = actionInfo;
                                    if (now.timeStamp - actionInfo['timeStamp'] > sjjg * 60 * 1000) {
                                        currentIndex++;
                                        if (currentIndex > camConfig.bscCameraYswVoList.length - 1) {
                                            currentIndex = 0;
                                        }
                                        changed = true;
                                    }
                                } else {
                                    changed = true;
                                }
                                if (changed === true) {
                                    let token = camConfig.bscCameraYswVoList[currentIndex]['token'];
                                    await this.Redis.set(`actionIndex_${ip}`, {
                                        actionIndex: currentIndex,
                                        actionInfo: {
                                            ...camConfig.bscCameraYswVoList[currentIndex],
                                            timeStamp: now.timeStamp
                                        }
                                    });
                                    if (devices[ip] === undefined) {
                                        connect(res => {
                                            if (res.error) {

                                            } else {
                                                setTimeout(() => {
                                                    gotoPreset(undefined, {
                                                        address: ip,
                                                        token,
                                                        Speed: { 'x': 1.0, 'y': 1.0, 'z': 1.0 }
                                                    })
                                                    configs.log(`gotoPreset`, `移动摄像机[${ip}]位到预置位 ${token}`, `info`);
                                                }, 500)
                                                configs.log(`connect`, `连接并注册自动轮询的摄像机 ${ip}`, `info`);
                                            }
                                        }, {
                                            address: ip,
                                            user: username,
                                            pass: password
                                        })
                                    } else {
                                        gotoPreset(undefined, {
                                            address: ip,
                                            token,
                                            Speed: { 'x': 1.0, 'y': 1.0, 'z': 1.0 }
                                        })
                                        configs.log(`gotoPreset`, `移动摄像机[${ip}]位到预置位 ${token}`, `info`)
                                    }
                                }
                            }
                            if (configs.ptzStopDelay) {
                                const ptzStop = await this.Redis.get(`ptzStop_${ip}`);
                                if (ptzStop && ptzStop !== '') {
                                    if (!isNaN(ptzStop) && (now.timeStamp - ptzStop > configs.ptzStopDelay * 1000)) {
                                        await this.Redis.remove(`ptzStop_${ip}`);
                                        let actionIndex = await this.Redis.get(`actionIndex_${ip}`) || ''
                                        let presetPosition = undefined;
                                        if (actionIndex) {
                                            if (actionIndex instanceof Object) {
                                                presetPosition = actionIndex['actionInfo'];
                                            }
                                        }
                                        if (presetPosition) {
                                            const ip2 = presetPosition['ip'] || '';
                                            const username2 = presetPosition['username'] || '';
                                            const password2 = presetPosition['password'] || '';
                                            const token2 = presetPosition['token'] || '';
                                            if (devices[ip2] === undefined) {
                                                connect(res => {
                                                    setTimeout(() => {
                                                        gotoPreset(undefined, {
                                                            address: ip2,
                                                            token: token2,
                                                            Speed: { 'x': 1.0, 'y': 1.0, 'z': 1.0 }
                                                        })
                                                        configs.log(`gotoPreset`, `[${ip2}]回到当前预置位 ${token2}`, `info`)
                                                    }, 500)
                                                    configs.log(`connect`, `连接并注册自动轮询的摄像机 ${ip2}`, `info`)
                                                }, {
                                                    address: ip2,
                                                    user: username2,
                                                    pass: password2
                                                })
                                            } else {
                                                gotoPreset(undefined, {
                                                    address: ip2,
                                                    token: token2,
                                                    Speed: { 'x': 1.0, 'y': 1.0, 'z': 1.0 }
                                                })
                                                configs.log(`gotoPreset`, `[${ip2}]回到当前预置位 ${token2}`, `info`)
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }, configs.execDelay * 1000)
        }
    }
}